package com.leilei;

import cn.hutool.core.collection.CollUtil;
import lombok.extern.log4j.Log4j2;
import org.locationtech.jts.algorithm.ConvexHull;
import org.locationtech.jts.geom.Coordinate;
import org.locationtech.jts.geom.Geometry;
import org.locationtech.jts.geom.GeometryFactory;
import org.locationtech.jts.operation.buffer.BufferOp;
import org.locationtech.jts.operation.buffer.BufferParameters;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;

/**
 * @author lei
 * @create 2023-04-06 09:29
 * @desc 定位点数据
 **/
@Log4j2
public final class CollectCalcUtil {

    /**
     * 经纬度倍数
     *
     * @author lei
     * @date 2023-02-27 17:28:10
     */
    public static final Double MULTIPLE = 1000000D;

    /**
     * 地球半径 m
     */
    public static final int EARTH_R = 6371000;
    /**
     * 定位点在指定线段上判定阈值
     */
    public static final double ON_LINE_DISTANCE = 1.0;

    /**
     * 最小多边形数据
     */
    public static final int MIN_POLYGON_SIZE = 3;

    public static final BufferParameters BUFFER_PARAMETER = getBufferParameters();

    private CollectCalcUtil() {
    }

    /**
     * 计算两个经纬度定位点之间的直线距离
     *
     * @param a A点
     * @param b B点
     * @return double m
     * @author lei
     * @date 2023-03-01 11:06:26
     */
    public static double distance(MapPoint a, MapPoint b) {
        double long1 = a.getLongitude() / MULTIPLE;
        double lat1 = a.getLatitude() / MULTIPLE;
        double long2 = b.getLongitude() / MULTIPLE;
        double lat2 = b.getLatitude() / MULTIPLE;
        return distance(lat1, long1, lat2, long2);
    }

    /**
     * 计算两个经纬度定位点之间的直线距离
     *
     * @param lat1
     * @param lng1
     * @param lat2
     * @param lng2
     * @return double
     * @author lei
     * @date 2023-03-01 11:06:26
     */
    public static double distance(double lat1, double lng1, double lat2, double lng2) {
        double dLat = Math.toRadians(lat2 - lat1);
        double dLng = Math.toRadians(lng2 - lng1);
        double a = Math.sin(dLat / 2) * Math.sin(dLat / 2) +
                Math.cos(Math.toRadians(lat1)) * Math.cos(Math.toRadians(lat2)) * Math.sin(dLng / 2) * Math.sin(dLng / 2);
        double c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
        return EARTH_R * c;
    }


    /**
     * 计算点是否在多边形区域内
     *
     * @param point              点
     * @param collectPolygonArea 多边形区域
     * @return 点是否在多边形区域内
     * @author shuiyun
     * @date 2023-02-20 10:08
     */
    public static <V> boolean locateInPolygonArea(MapPoint point, CollectPolygonArea<V> collectPolygonArea) {
        LinkList<MapPoint> mapPoints = collectPolygonArea.getPoints();
        return locateInPolygonArea(point, mapPoints);
    }


    // /**
    //  * 判断是否在多边形区域内
    //  *
    //  * @param point     点
    //  * @param mapPoints 多边形区域点
    //  * @return 判断是否在多边形区域内
    //  * @author shuiyun
    //  * @date 2023-03-13 14:54
    //  */
    // private static boolean locateInPolygonArea(MapPoint point, LinkList<MapPoint> mapPoints) {
    //     int crossCount = 0;
    //     long count = mapPoints.getSize();
    //     Node<MapPoint> node = mapPoints.getFirst();
    //     for (int i = 0; i < count; i++) {
    //         MapPoint a = node.getData();
    //         MapPoint b = node.getNext().getData();
    //         node = node.getNext();
    //         // 求解 y=p.y 与 ab 的交点
    //         // ab 与 y=p0.y平行
    //         if (a.getLatitude().equals(b.getLatitude())) {
    //             continue;
    //         }
    //         // 交点在ab延长线上
    //         if (point.getLatitude() < Math.min(a.getLatitude(), b.getLatitude())) {
    //             continue;
    //         }
    //         // 交点在ab延长线上
    //         if (point.getLatitude() >= Math.max(a.getLatitude(), b.getLatitude())) {
    //             continue;
    //         }
    //         // 求交点的 X 坐标
    //         double x = (double) (point.getLatitude() - a.getLatitude()) * (double) (b.getLongitude() - a.getLongitude())
    //                 / (b.getLatitude() - a.getLatitude()) + a.getLongitude();
    //         // 只统计单边交点
    //         if (x > point.getLongitude()) {
    //             crossCount++;
    //         }
    //     }
    //     return (crossCount % 2 == 1);
    // }


    /**
     * 计算指定定位点是否在多边形中
     *
     * @param point
     * @param mapPoints
     * @return boolean
     * @author lei
     * @date 2023-04-10 17:44:33
     */
    private static boolean locateInPolygonArea(MapPoint point, LinkList<MapPoint> mapPoints) {
        int crossCount = 0;
        MapPoint prevPoint = mapPoints.getLast().getData();
        List<MapPoint> list = mapPoints.getList();
        int latitude = point.getLatitude();
        int longitude = point.getLongitude();
        int prevPointLatitude = prevPoint.getLatitude();
        int prevPointLongitude = prevPoint.getLongitude();
        for (MapPoint currPoint : list) {
            // 求解 y=p.y 与 ab 的交点
            // ab 与 y=p0.y平行
            int currPointLatitude = currPoint.getLatitude();
            int currPointLongitude = currPoint.getLongitude();
            if (prevPointLatitude == currPointLatitude) {
                prevPointLongitude = currPointLongitude;
                continue;
            }
            // 交点在ab延长线上
            if (latitude < Math.min(prevPointLatitude, currPointLatitude) ||
                    latitude >= Math.max(prevPointLatitude, currPointLatitude)) {
                prevPointLatitude = currPointLatitude;
                prevPointLongitude = currPointLongitude;
                continue;
            }
            // 求交点的 X 坐标
            double diffLatitude = (double) currPointLatitude - prevPointLatitude;
            double x = (latitude - prevPointLatitude) * (currPointLongitude - prevPointLongitude)
                    / diffLatitude + prevPointLongitude;
            // 只统计单边交点
            if (x > longitude) {
                crossCount++;
            }
            prevPointLatitude = currPointLatitude;
            prevPointLongitude = currPointLongitude;
        }
        return (crossCount % 2 == 1);
    }


    /**
     * 点到直线的最短距离的判断 点（x0,y0） 到由两点组成的线段（x1,y1） ,( x2,y2 )
     *
     * @param startPoint 线段A点
     * @param endPoint   线段B点
     * @param point      端点
     * @return 点到线段的距离
     * @author shuiyun
     * @date 2023-02-20 10:35
     */
    public static double pointToLine(MapPoint startPoint, MapPoint endPoint, MapPoint point) {
        double aLat = startPoint.getLatitude() / MULTIPLE;
        double aLng = startPoint.getLongitude() / MULTIPLE;
        double bLat = endPoint.getLatitude() / MULTIPLE;
        double bLng = endPoint.getLongitude() / MULTIPLE;
        double cLat = point.getLatitude() / MULTIPLE;
        double cLng = point.getLongitude() / MULTIPLE;
        double space;
        double a;
        double b;
        double c;
        // 线段的长度
        a = distance(aLat, aLng, bLat, bLng);
        // (x1,y1)到点的距离
        b = distance(aLat, aLng, cLat, cLng);
        // (x2,y2)到点的距离
        c = distance(bLat, bLng, cLat, cLng);
        if (c <= 1 || b <= 1) {
            space = 0;
            return space;
        }
        if (a <= 1) {
            space = b;
            return space;
        }
        if (c * c >= a * a + b * b) {
            space = b;
            return space;
        }
        if (b * b >= a * a + c * c) {
            space = c;
            return space;
        }
        // 半周长
        double p = (a + b + c) / 2;
        // 海伦公式求面积
        double s = Math.sqrt(p * (p - a) * (p - b) * (p - c));
        // 返回点到线的距离（利用三角形面积公式求高）
        space = 2 * s / a;
        return space;
    }


    /**
     * 检查指定点是否在两点组成的线段上
     *
     * @param pointA
     * @param pointB
     * @param point
     * @return boolean
     * @author lei
     * @date 2023-03-01 11:08:40
     */
    public static boolean pointOnLineSegment(MapPoint pointA, MapPoint pointB, MapPoint point) {
        return pointOnLine(pointA.getLatitude() / MULTIPLE, pointA.getLongitude() / MULTIPLE,
                pointB.getLatitude() / MULTIPLE, pointB.getLongitude() / MULTIPLE,
                point.getLatitude() / MULTIPLE, point.getLongitude() / MULTIPLE);
    }

    private static boolean pointOnLine(double startLat, double startLng, double endLat, double endLng, double pointLat, double pointLng) {
        // 检查点是否在最小和最大纬度之间
        double minLat = Math.min(startLat, endLat);
        double maxLat = Math.max(startLat, endLat);
        if (pointLat < minLat || pointLat > maxLat) {
            return false;
        }

        // 检查点是否在最小和最大经度之间
        double minLng = Math.min(startLng, endLng);
        double maxLng = Math.max(startLng, endLng);
        if (pointLng < minLng || pointLng > maxLng) {
            return false;
        }

        // 计算点到线段的距离
        double d1 = distance(pointLat, pointLng, startLat, startLng);
        double d2 = distance(pointLat, pointLng, endLat, endLng);
        double d3 = distance(startLat, startLng, endLat, endLng);
        // 距离阈值 设置为0.1m
        double buffer = ON_LINE_DISTANCE;
        // 如果点到线段的距离小于阈值，则认为它在线段上
        return d1 + d2 >= d3 - buffer && d1 + d2 <= d3 + buffer;
    }

    /**
     * 求两定位点中点
     *
     * @param one
     * @param two
     * @return MapPoint
     * @author lei
     * @date 2023-02-27 14:43:31
     */
    public static MapPoint getMidpoint(MapPoint one, MapPoint two) {
        double a1 = Math.toRadians(one.getLongitude() / MULTIPLE);
        double b1 = Math.toRadians(one.getLatitude() / MULTIPLE);
        double a2 = Math.toRadians(two.getLongitude() / MULTIPLE);
        double b2 = Math.toRadians(two.getLatitude() / MULTIPLE);
        double d = a2 - a1;
        double x = Math.cos(b2) * Math.cos(d);
        double y = Math.cos(b2) * Math.sin(d);
        double a = a1 + Math.atan2(y, Math.cos(b1) + x);
        double b = Math.atan2(Math.sin(b1) + Math.sin(b2), Math.sqrt((Math.cos(b1) + x) * (Math.cos(b1) + x) + y * y));
        double finalLong = Math.toDegrees(a);
        double finalLat = Math.toDegrees(b);
        return MapPoint.builder().longitude((int) (finalLong * MULTIPLE)).latitude((int) (finalLat * MULTIPLE)).build();
    }

    /**
     * 求一堆定位点中心点坐标
     *
     * @param points
     * @return MapPoint
     * @author lei
     * @date 2023-02-27 14:43:51
     */
    public static MapPoint getCenterPoint(List<MapPoint> points) {
        if (CollUtil.isEmpty(points)) {
            return null;
        }
        int total = points.size();
        double x = 0;
        double y = 0;
        double z = 0;
        for (MapPoint mapPoint : points) {
            double lat = mapPoint.getLatitude() / MULTIPLE * Math.PI / 180;
            double lon = mapPoint.getLongitude() / MULTIPLE * Math.PI / 180;
            x += Math.cos(lat) * Math.cos(lon);
            y += Math.cos(lat) * Math.sin(lon);
            z += Math.sin(lat);
        }
        x = x / total;
        y = y / total;
        z = z / total;
        double lon2 = Math.atan2(y, x);
        double hyp = Math.sqrt(x * x + y * y);
        double lat2 = Math.atan2(z, hyp);
        MapPoint centerPoint = new MapPoint();
        centerPoint.setLongitude((int) ((lon2 * 180 / Math.PI) * MULTIPLE));
        centerPoint.setLatitude((int) ((lat2 * 180 / Math.PI) * MULTIPLE));
        return centerPoint;
    }

    /**
     * 计算CAB点夹角余弦值 如果余弦值大于0则为锐角 若余弦小于0则为钝角 若等于0则为直角
     * 余弦公式 cosA = (b^2 + c^2 - a^2)/(2bc)
     * 其中b = A-C线段距离 c = A-B线段距离 a = B-C线段距离
     *
     * @param c C点
     * @param a A点
     * @param b B点
     * @return 余弦值
     * @author shuiyun
     * @date 2023-02-20 11:05
     */
    public static double cosValue(MapPoint c, MapPoint a, MapPoint b) {
        double cLat = c.getLatitude() / MULTIPLE;
        double cLng = c.getLongitude() / MULTIPLE;
        double aLat = a.getLatitude() / MULTIPLE;
        double aLng = a.getLongitude() / MULTIPLE;
        double bLat = b.getLatitude() / MULTIPLE;
        double bLng = b.getLongitude() / MULTIPLE;
        // 选择计算后的经纬度进行距离计算,可减少重复计算逻辑
        double lineA = distance(bLat, bLng, cLat, cLng);
        double lineB = distance(aLat, aLng, cLat, cLng);
        double lineC = distance(bLat, bLng, aLat, aLng);
        return (lineB * lineB + lineC * lineC - lineA * lineA) / (2 * lineB * lineC);
    }


    /**
     * 根据当前传入点,尝试聚集点参数修正
     *
     * @param collectPolygonArea 多边形区域
     * @param curPoint           当前点
     * @return
     * @author lei
     * @date 2023-03-13 11:20
     */
    public static <V> void reviseArea(CollectPolygonArea<V> collectPolygonArea, MapPoint curPoint) {
        int beforeMaxLong = collectPolygonArea.getMaxLongitude();
        int beforeMaxLat = collectPolygonArea.getMaxLatitude();
        int beforeMinLong = collectPolygonArea.getMinLongitude();
        int beforeMinLat = collectPolygonArea.getMinLatitude();

        int curMaxLong = Math.max(beforeMaxLong, curPoint.getLongitude());
        int curMaxLat = Math.max(beforeMaxLat, curPoint.getLatitude());
        int curMinLong = Math.min(beforeMinLong, curPoint.getLongitude());
        int curMinLat = Math.min(beforeMinLat, curPoint.getLatitude());
        // 尝试重新计算最大最小经纬 圆心
        if (curMaxLong != beforeMaxLong || curMaxLat != beforeMaxLat || curMinLong != beforeMinLong || curMinLat != beforeMinLat) {
            setAreaParam(collectPolygonArea, curMaxLong, curMinLong, curMaxLat, curMinLat);
        }
    }


    /**
     * 基于聚集区内部数据自动修正参数
     *
     * @param area
     * @return void
     * @author lei
     * @date 2023-04-18 17:36:15
     */
    public static <V> void reviseArea(CollectPolygonArea<V> area) {
        // 删除了定边点
        List<MapPoint> mapPoints = area.getPoints().getList();
        int maxLng = Integer.MIN_VALUE;
        int maxLat = Integer.MIN_VALUE;
        int minLng = Integer.MAX_VALUE;
        int minLat = Integer.MAX_VALUE;
        for (MapPoint point : mapPoints) {
            int lng = point.getLongitude();
            int lat = point.getLatitude();
            maxLng = Math.max(maxLng, lng);
            maxLat = Math.max(maxLat, lat);
            minLng = Math.min(minLng, lng);
            minLat = Math.min(minLat, lat);
        }
        setAreaParam(area, maxLng, minLng, maxLat, minLat);
    }

    /**
     * 设置区域内部参数
     *
     * @param collectPolygonArea
     * @param maxLng
     * @param minLng
     * @param maxLat
     * @param minLat
     * @return void
     * @author lei
     * @date 2023-03-09 17:02:34
     */
    private static <V> void setAreaParam(CollectPolygonArea<V> collectPolygonArea, Integer maxLng, Integer minLng, Integer maxLat, Integer minLat) {
        collectPolygonArea.setMaxLongitude(maxLng);
        collectPolygonArea.setMaxLatitude(maxLat);
        collectPolygonArea.setMinLongitude(minLng);
        collectPolygonArea.setMinLatitude(minLat);
        MapPoint maxLongLatPoint = new MapPoint(maxLng, maxLat);
        MapPoint midpoint = CollectCalcUtil.getMidpoint(maxLongLatPoint, new MapPoint(minLng, minLat));
        collectPolygonArea.setCycleCenter(midpoint);
        collectPolygonArea.setRadius(CollectCalcUtil.distance(midpoint, maxLongLatPoint));
    }

    /**
     * 清除内凹角
     *
     * @param area
     * @return void
     * @author lei
     * @date 2023-03-20 14:01:36
     */
    public static <V> void removeConcavePoints(CollectPolygonArea<V> area) {
        LinkList<MapPoint> points = area.getPoints();
        long beforeSize = points.getSize();
        if (beforeSize <= MIN_POLYGON_SIZE) {
            return;
        }
        Node<MapPoint> node = points.getFirst();
        HashMap<MapPoint, Coordinate> pointMap = new HashMap<>(20);
        Coordinate[] coordinates = points.getList().stream()
                .map(x -> {
                    Coordinate coordinate = new Coordinate(x.getLongitude(), x.getLatitude());
                    pointMap.put(x, coordinate);
                    return coordinate;
                }).toArray(Coordinate[]::new);
        // 计算凸包
        ConvexHull convexHull = new ConvexHull(coordinates, new GeometryFactory());
        Geometry geometry = convexHull.getConvexHull();
        Set<Coordinate> convexSet = Arrays.stream(geometry.getCoordinates()).collect(Collectors.toSet());
        for (long i = 0L; i < points.getSize(); i++) {
            Node<MapPoint> next = node.getNext();
            MapPoint data = node.getData();
            Coordinate coordinate = pointMap.get(data);
            // 移除内凹角
            if (coordinate != null && !convexSet.contains(coordinate)) {
                points.del(node);
            }
            node = next;
        }
        long curSize = points.getSize();
        if (curSize != beforeSize) {
            reviseArea(area);
        }
    }


    /**
     * 定边点排序 避免边交叉
     *
     * @param points
     * @return List<MapPoint>
     * @author lei
     * @date 2023-03-20 15:56:04
     */
    public static List<MapPoint> sortPoints(List<MapPoint> points) {
        if (CollUtil.isEmpty(points)) {
            return new ArrayList<>();
        }
        if (points.size() <= 1) {
            // 如果点集合为空或只有一个点,则直接返回
            return points;
        }
        // log.debug("修正前区域组成顺序：{}", points.stream().map(MapPoint::getPointName).collect(Collectors.joining("-> ")));
        // 选取起点 选择经度最小的点作为起点，如果经度相同，则选择纬度最小的点作为起点
        MapPoint start = points.get(0);
        for (MapPoint p : points) {
            if (p.getLongitude() < start.getLongitude() || (p.getLongitude().equals(start.getLongitude()) && p.getLatitude() < start.getLatitude())) {
                start = p;
            }
        }
        // 计算起点到其他点的极角并排序
        List<MapPoint> sortedPoints = new ArrayList<>(points);
        sortedPoints.remove(start);
        MapPoint finalStart = start;
        sortedPoints.sort((p1, p2) -> {
            double p1Lat = p1.getLatitude() / CollectCalcUtil.MULTIPLE;
            double p1Lng = p1.getLongitude() / CollectCalcUtil.MULTIPLE;
            double p2Lat = p2.getLatitude() / CollectCalcUtil.MULTIPLE;
            double p2Lng = p2.getLongitude() / CollectCalcUtil.MULTIPLE;
            double startLat = finalStart.getLatitude() / CollectCalcUtil.MULTIPLE;
            double startLng = finalStart.getLongitude() / CollectCalcUtil.MULTIPLE;
            // 计算两个点与起点的极值
            double angle1 = Math.atan2(p1Lat - startLat, p1Lng - startLng);
            double angle2 = Math.atan2(p2Lat - startLat, p2Lng - startLng);
            // 说明p1点在p2点的左侧，p1点应该排在p2点的前面
            if (angle1 < angle2) {
                return -1;
            }
            // 说明p1点在p2点的右侧，p1点应该排在p2点的后面
            if (angle1 > angle2) {
                return 1;
            }
            // 极值相等则p1 p2在同一条直线上,根据其与起点距离进行排序
            double distance = CollectCalcUtil.distance(p1, finalStart);
            double distance1 = CollectCalcUtil.distance(p2, finalStart);
            return Double.compare(distance, distance1);
        });

        // 加入起点并返回
        sortedPoints.add(0, start);
        // log.debug("修正后区域组成顺序：{}", sortedPoints.stream().map(MapPoint::getPointName).collect(Collectors.joining("-> ")));
        return sortedPoints;
    }

    /**
     * 多边形区域扩展
     *
     * @param points         定位点
     * @param extendDistance 外扩距离 m
     * @return List<MapPoint>
     * @author lei
     * @date 2023-03-21 16:19:30
     */
    public static List<MapPoint> extendPoints(List<MapPoint> points, double extendDistance) {
        List<MapPoint> extendsPoints = points;
        if (extendDistance > 1.0 && points.size() >= MIN_POLYGON_SIZE) {
            Coordinate[] coordinates = points.stream().map(point -> new Coordinate(point.getLongitude(), point.getLatitude()))
                    .toArray(Coordinate[]::new);
            ConvexHull convexHull = new ConvexHull(coordinates, new GeometryFactory());
            Geometry geometry = convexHull.getConvexHull();
            Geometry expandedPolygon = BufferOp.bufferOp(geometry, extendDistance * 10, BUFFER_PARAMETER);
            Coordinate[] extendArray = expandedPolygon.convexHull().getCoordinates();
            extendsPoints = Arrays.stream(extendArray).map(x -> new MapPoint((int) x.x, (int) x.y)).distinct().collect(Collectors.toList());
        }
        return extendsPoints;
    }

    /**
     * 填充扩展参数
     *
     * @param
     * @return BufferParameters
     * @author lei
     * @date 2023-03-21 16:34:37
     */
    private static BufferParameters getBufferParameters() {
        // 每个圆弧的线段数,值越大夹角越平滑,
        int quadrantSegments = 1;
        // 端点类型
        int endCapStyle = BufferParameters.CAP_ROUND;
        // 连接类型
        int joinStyle = BufferParameters.JOIN_ROUND;
        // 指定斜接连接的斜率限制 当两个线段夹角很小时,斜接点锐角非常尖锐; 避免锐化可限制斜接点的斜率,当斜率超过指定阈值将强使用JOIN_BEVEL
        double mitreLimit = 1.0;
        BufferParameters bufferParams = new BufferParameters(quadrantSegments, endCapStyle, joinStyle, mitreLimit);
        bufferParams.setSingleSided(true);
        return bufferParams;
    }

}

